/*
 * Menu: ActionScript > PureMVC > Generator
 * Key: M3+2
 * Kudos: Lance Woodson
 * DOM: http://localhost/org.eclipse.eclipsemonkey.lang.javascript
 */

var editor;
var source;
var originalLength;
var sourceLines;
var className;
var superclassName;
var packageName;
var importInsertionPoint;
var bodyInsertionPoint;
var constructorExists;
var constructorStartPoint;
var constructorEndPoint;

function main() {
	// Initialize data.
	var handlers = {
		Proxy: proxyHandler,
		SimpleCommand: commandHandler,
		MacroCommand: macroCommandHandler,
		Mediator: mediatorHandler,
		Facade: facadeHandler
	}
	editor = editors.activeEditor;
	source = editor.source;
	originalLength = source.length;
	sourceLines = editor.source.split("\n");
	className = extractClass(source);
	superclassName = extractSuperclass(source);
	packageName = extractPackageName(source);
	factorInsertionPoints(source);
	
	// Delegate code generation to function specific to superclass.
	var code = {
		imports: [],
		constants: [],
		constructor: getDefaultConstructor(),
		memberVars: [],
		methods: []
	};
	var f = handlers[superclassName];
	if (f == undefined) {
		throw "No handler found for superclass " + superclassName;
	} else {
		f(code);
	}
	
	// Inject generated code.
	if (code.methods && code.methods.length > 0) {
		code.methods.pop();
		insert(sourceLines, code.methods, bodyInsertionPoint);
	}
	if (code.constructor) {
		insert(sourceLines, code.constructor, bodyInsertionPoint);
	}
	if (code.memberVars && code.memberVars.length > 0) {
		insert(sourceLines, code.memberVars, bodyInsertionPoint);
	}
	if (code.constants && code.constants.length > 0) {
		insert(sourceLines, code.constants, bodyInsertionPoint);
	}
	if (code.imports && code.imports.length > 0) {
		insert(sourceLines, code.imports, importInsertionPoint);
	}
	
	modifyEditorSource();
}

function proxyHandler(code) {	
	deleteConstructor();
	
	var holder = {
			successNotificationName: null,
			failureNotificationName: null,
			destinationName: null,
			remoteMethod: null
	};
	code.constants.push(buildStringConstant(className, "NAME"));
	var options = [
	    {
	    	name: 'successNotificationName', 
	    	query: 'Enter success notification name', 
	    	value: null,
	    	populator: function() {
	    		code.constants.push(buildStringConstant(this.value));
	    		holder.successNotificationName = this.value;
	    	}
	    },
	    {
	    	name: 'failureNotificationName', 
	    	query: 'Enter failure notification name', 
	    	value: null,
	    	populator: function() {
	    		code.constants.push(buildStringConstant(this.value));
	    		holder.failureNotificationName = this.value;
	    	}
	    },
	    {
	    	name: 'remoteObjectDestination',
	    	query: 'Enter the remote object detination (spring bean ID)',
	    	value: null,
	    	populator: function() {
	    		code.constructor = "\t\tpublic function " + className + "()\n" +
	    		                   "\t\t{\n" +
	    		                   "\t\t\tsuper(NAME, null)\n" +
	    		                   "\t\t\t" + this.value + " = new RemoteObject();\n" +
	    		                   "\t\t\t" + this.value + ".destination = \"" + this.value + "\";\n" +
	    		                   "\n\t\t}\n";
	    		holder.destinationName = this.value;
	    	}
	    },
	    {
	    	name: 'remoteMethod',
	    	query: 'Enter the name of the remote method you want to invoke',
	    	value: null,
	    	populator: function() {
	    		holder.remoteMethod = this.value;
	    	}
	    },
	    {
	    	name: 'remoteArgs',
	    	query: 'Enter an arguments list for the remote method: arg1:Type, arg2:Type, ...',
	    	value: null,
	    	populator: function() {
	    		var remoteMethod = buildMethodSignature("public", holder.remoteMethod, this.value, "void") +
	    		                   "\t\t{\n" +
	    		                   "\t\t\t" + holder.destinationName + "." + holder.remoteMethod + "(" + buildInvocationParams(this.value) + ");\n" +
	    		                   "\t\t}\n";
	    		code.methods.push(remoteMethod);
	    		code.methods.push("\n");
	    	}
	    }
	];
	queryToPopulate(options);
	
	for(var i = 0; i < options.length; i++) {
		options[i].populator();
	}
	code.memberVars.push(buildMemberVar(holder.destinationName + ':RemoteObject'));
	var resultHandler = buildMethodSignature("private", "onResult", "event:ResultEvent", "void") +
	                    "\t\t{\n" +
						"\t\t\tsetData(event.result);\n" +
						"\t\t\tsendNotification(" + holder.successNotificationName + ", event.result);\n" +
						"\t\t}\n";
	code.methods.push(resultHandler);
	code.methods.push("\n");
	var faultHandler = buildMethodSignature("private", "onFault", "event:FaultEvent", "void") +
						"\t\t{\n" +
						"\t\t\tsendNotification(" + holder.failureNotificationName + ", event.fault.faultString);\n" +
						"\t\t}\n";
	code.methods.push(faultHandler);
	code.methods.push("\n");
	code.imports.push("\timport mx.rpc.remoting.mxml.RemoteObject;\n");
	code.imports.push("\timport mx.rpc.events.ResultEvent;\n");
	code.imports.push("\timport mx.rpc.events.FaultEvent;\n");	
}

/**
 * Handles customization of Command subclasses.
 */
function commandHandler(code) {
	deleteConstructor();
	
	var executeMethod = buildMethodSignature("override public", "execute", "note:INotification", "void") +
	                    "\t\t{\n" +
	                    "\t\t\t// TODO interact with proxies or mediators through facade\n" +
	                    "\t\t}";
	code.methods.push(executeMethod);
	code.methods.push("\n");
	code.imports.push("\timport org.puremvc.as3.multicore.interfaces.INotification;\n");
}

/**
 * Handles customization of MacroCommands subclasses.
 */
function macroCommandHandler(code) {
	deleteConstructor();
	var macroCommands = [];
	var subCommand = null;
	do {
		subCommand = gatherInput("Specify Subcommands", "Specify a sub-command's class name to add to the macro command, or leave the field empty if done", null);
		if (subCommand != "") macroCommands.push(subCommand);
	} while(subCommand != "");
	var initializeMethod = buildMethodSignature("override protected", "initializeMacroCommand", "", "void") +
						"\t\t{\n";
	for (var i = 0; i < macroCommands.length; i++) {
		initializeMethod = initializeMethod + 
		                   "\t\t\taddSubCommand(" + macroCommands[i] + ");\n";
	}
	initializeMethod = initializeMethod + "\t\t}\n";
	code.methods.push(initializeMethod);
	code.methods.push("\n");
}

function mediatorHandler(code) {
	deleteConstructor();
	
	code.constants.push(buildStringConstant(className, "NAME"));
	var notificationInterests = []
	var interest;
	do {
		interest = gatherInput("Specify Notification Interests", "Specify notifications that should be sent to the mediator (ex:  SomeClass.NOTIFICATION_CONST)", null);
		if (interest != "") notificationInterests.push(interest);
	} while (interest != "");
	var registerInterests = buildMethodSignature("override public", "listNotificationInterests", "", "Array") + 
	                        "\t\t{\n" +
	                        "\t\t\treturn [\n";
	for (var i = 0; i < notificationInterests.length; i++) {
		registerInterests = registerInterests + "\t\t\t\t" + notificationInterests[i];
		if (i < notificationInterests.length - 1) {
			registerInterests = registerInterests + ",\n";
		}
	}
	registerInterests = registerInterests + "\n\t\t\t];\n\t\t}\n";
	code.methods.push(registerInterests);
	code.methods.push("\n");
	
	// Define notification handler
	var handleNotifications = buildMethodSignature("override public", "handleNotification", "notification:INotification", "void") + 
	                          "\t\t{\n" + 
	                          "\t\t\tswitch(notification.getName()) {\n";
	for (var i = 0; i < notificationInterests.length; i++) {
		handleNotifications = handleNotifications +
		                      "\t\t\t\tcase " + notificationInterests[i] + ":\n" +
		                      "\t\t\t\t\t// TODO implement handling\n" +
							  "\t\t\t\t\tbreak;\n"
	}
	handleNotifications = handleNotifications +
	                      "\t\t\t}\n" +
	                      "\t\t}\n";
	code.methods.push(handleNotifications);
	code.methods.push("\n");
	
	// Define ui component event handlers
	var events =  {};
	var eventHandler;
	do {
		eventHandler = gatherInput("Specify event details", "Enter any UIComponent events to be register this mediator for in the format eventName[:eventType].  Example: click:MouseEvent", null);
		if (eventHandler != "") {
			var parts = eventHandler.split(':');
			debug(parts);
			var eventName = parts[0];
			if (parts[0].indexOf('.') > 0) {
				debug(parts[0]);
				eventName = parts[0].substring(parts[0].indexOf('.')+1);
				debug(eventName);
			}
			if (parts.length < 2) {
				parts = [parts[0], "Event"];
			}
			var methodName = "handle" + eventName.toCamelCase().capitalized();
			var handlerMethod = buildMethodSignature("public", methodName, "event:" + parts[1], "void") +
				                "\t\t\t// TODO implement UI Component event handling\n" +
				                "\t\t}\n"
			code.methods.push(handlerMethod);
			code.methods.push("\n");
			events[parts[0]] = methodName;
		}
	} while(eventHandler != "");
	
	code.constructor = "\t\tpublic function " + className + "(component:UIComponent)\n" +
					  "\t\t{\n" +
	                  "\t\t\tsuper(NAME, component);\n";
	for(var event in events) {
		code.constructor = code.constructor + 
				"\t\t\tcomponent.addEventListener(" + event + ", " + events[event] + ");\n";
	}
	code.constructor = code.constructor + "\t\t}\n";
	                  
	// TODO imports
}

function facadeHandler(code) {
	debug("Facades not yet supported");
}

function modifyEditorSource() {
	source = sourceLines.join("\n");
	editor.applyEdit(0, originalLength, source);
}

function deleteConstructor() {
	sourceLines.splice(bodyInsertionPoint, (constructorEndPoint - bodyInsertionPoint + 1));
}

function getDefaultConstructor() {
	return sourceLines.slice(bodyInsertionPoint, 
			(constructorEndPoint - bodyInsertionPoint + 1)).join('\n');
}

function buildInvocationParams(paramList) {
	var paramTypeList = paramList.split(',');
	var invocationParams = [];
	for(var i = 0; i < paramTypeList.length; i++) {
		invocationParams.push(paramTypeList[i].split(':')[0]);
	}
	return invocationParams;
}

function buildStringConstant(name, varName) {
	if (typeof(varName) == 'undefined') {
		varName = name;
	}
	return "\t\tpublic static const " + varName + ":String = \"" + name + "\";\n";
}

function buildMemberVar(name) {
	var parts = name.split(':');
	return "\t\tprivate var " + parts[0] + ":" + parts[1] + ";\n"
}

function buildMethodSignature(visibility, name, args, returnType) {
	return "\t\t" + visibility + " function " + name + "(" + args + "):" + returnType + "\n";
}

function factorInsertionPoints() {
	var bracketCount = 0;
	var constStarted = false;
	var constructorRegex = new RegExp("function " + className + "\\(");
	var openBracketRegex = new RegExp("\\{");
	var closeBracketRegex = new RegExp("\\}");
	for(var i = 0; i < sourceLines.length; i++) {
		/*
		if (/import/.exec(sourceLines[i])) {
			importInsertionPoint = i+1;
		}
		*/
		if (/ class /.exec(sourceLines[i])) {
			importInsertionPoint = i - 2;
		}
		
		if (constructorRegex.exec(sourceLines[i])) {
			constructorExists = true;
			constructorStartPoint = i;
		}
		
		if (openBracketRegex.exec(sourceLines[i])) {
			bracketCount++;
			if (bracketCount == 2) {
				bodyInsertionPoint = i+1;		
			}
		}
				
		if (constructorExists && closeBracketRegex.exec(sourceLines[i])) {
			constructorEndPoint = i;
			break;
		}
	}
}

function extractClass(source) {
	var regexp = new RegExp(" class (\\w+) ");
	try {
		return regexp.exec(source)[1];
	} catch (ex) {
		throw "Unable to parse class name";
	}
}

function extractSuperclass(source) {
	var regexp = new RegExp(" extends (\\w+)");
	try {
		return regexp.exec(source)[1];
	} catch (ex) {
		throw "Unable to parse superclass name";
	}
}

function extractPackageName(source) {
	var regexp = new RegExp("package ([\\w\\.]+)");
	try {
		return regexp.exec(source)[1];
	} catch (ex) {
		throw "Unable to parse package name";
	}
}

function queryToPopulate(dataObjects) {
	for (var i = 0; i < dataObjects.length; i++) {
		dataObjects[i].value = gatherInput("Define Variables", dataObjects[i].query, null);
	}
	return dataObjects;
}

function gatherInput(label, queryString, defaultValue) {
	dialog = new Packages.org.eclipse.jface.dialogs.InputDialog(
	          window.getShell(), 
	          label,
	          queryString,
	          null, null
	);
	result = dialog.open()
	// if (result == Packages.org.eclipse.jface.window.Window.OK)
		return dialog.getValue();
}

function debug(s) {
	Packages.org.eclipse.jface.dialogs.MessageDialog.openInformation(window.getShell(), "Monkey Debugging", s);
}

function insert(source, targetArr, position, demark) {
	var target = targetArr;
	if (targetArr.join == undefined) {
		target = [targetArr];
	}
	target = target.join('');
	
	if (typeof(demark) != 'undefined') source.splice(position, 0, "-- splice end --");
	source.splice(position, 0, target);
	if (typeof(demark) != 'undefined') source.splice(position, 0, "-- splice start at " + position + " --");
}

String.prototype.capitalized = function(){
	return this.substring(0, 1).toUpperCase() + this.slice(1);
};

String.prototype.uncapitalize = function() {
	return this.substring(0, 1).toLowerCase() + this.slice(1);
}

String.prototype.toCamelCase = function() {
	var capitalize = false;
	var temp = "";
	var element;
	for (var i = 0; i < this.length(); i++) {
		element = this.substring(i, i+1);
		if (capitalize) {
			temp = temp + element.capitalized();
			capitalize = false;
		} else if (element == '_') {
			capitalize = true;
		} else {
			temp = temp + element.toLowerCase();
		}
	}
	return temp;
};